ToString解决方案

背景

提供一个通用机制 UStringUtils::ToString(Value) 将其 String 化。

其中 Value 包括:

  1. Base:基础类型(intboolfloatFString 等);
  2. Enum
  3. 包含 ToStringGetUID 方法的类型、指针;
  4. PointerTStrongPtrTShaerdPtrTWeakPtrTUniquePtr
  5. Variant
  6. Container:包括 ArraySetMap 等;

实现

基础类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
template<typename T>
static typename TEnableIf<TIsIntegral<T>::Value, FString>::Type ToString(const T& Value)
{
return FString::FromInt(static_cast<int32>(Value));
}

static FString ToString(const uint32& Value)
{
return FString::Printf(TEXT("%u"), Value);
}

static FString ToString(const int64& Value)
{
return FString::Printf(TEXT("%lld"), Value);
}

static FString ToString(const uint64& Value)
{
return FString::Printf(TEXT("%llu"), Value);
}

template<typename T>
static typename TEnableIf<TIsFloatingPoint<T>::Value, FString>::Type ToString(const T& Value)
{
return FString::Printf(TEXT("%.3f"), static_cast<float>(Value));
}

static FString ToString(const bool& Value)
{
return FString::Printf(TEXT("%s"), Value ? TEXT("true") : TEXT("false"));
}

static FString ToString(const FString& Value)
{
return Value;
}

Enum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template<typename T>
static typename TEnableIf<TIsEnum<T>::Value, FString>::Type ToString(const T& Value)
{
if constexpr (google::protobuf::is_proto_enum<T>::value)
{
auto String = ::google::protobuf::internal::NameOfEnum(::google::protobuf::template GetEnumDescriptor<T>(), static_cast<int>(Value));
return UTF8_TO_TCHAR(String.c_str());
}
else if constexpr (TIsUEnumClass<T>::Value)
{
auto EnumClass = StaticEnum<T>();
return EnumClass->GetNameStringByValue(static_cast<int64>(Value));
}
else
{
return FString::Printf(TEXT("RawEnum_%s"), *ToString(static_cast<__underlying_type(T)>(Value)));
}
}

template<typename T>
static typename TEnableIf<TIsEnum<T>::Value, FString>::Type ToString(const TEnumAsByte<T>& Value)
{
return ToString(Value.GetValue());
}

注意,这里用到了 TIsUEnumClass 判断是否是 UEnum,这在 UE5 才正式提供,具体代码在 UhtHeaderCodeGeneratorHFile.cs : AppendEnum 中生成;

如果是 UE4,可以自定义该 concept,修改 UHTCodeGenerator ,将其 UEnum : TIsUEnumClass = true生成在 .generated.h 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. 新增 TIsUEnumClass
template <typename T>
struct TIsUEnumClass
{
static constexpr bool Value = false;
};

// 2. 修改 UEnum 的 Genrator

void FNativeClassHeaderGenerator::ExportEnum(FOutputDevice& Out, UEnum* Enum) const
{
// ...

if (const EUnderlyingEnumType* EnumPropType = GEnumUnderlyingTypes.Find(Enum))
{
// ...
Out.Logf( TEXT("\r\n") );
Out.Logf( TEXT("enum class %s%s;\r\n"), *Enum->CppType, *UnderlyingTypeString );
Out.Logf( TEXT("template<> %sUEnum* StaticEnum<%s>();\r\n"), *GetAPIString(), *Enum->CppType );
Out.Logf( TEXT("\r\n") );


// 新增 TIsUEnumClass 判断
Out.Logf(TEXT("template<> struct TIsUEnumClass<%s> { static constexpr bool Value = true; };\r\n"), *Enum->CppType);
Out.Logf(TEXT("\r\n"));
}
}

包含 ToStringGetUID 方法的类型、指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
template<typename T>
static typename TEnableIf<THasToStringFunc<T>::Value, FString>::Type ToString(const T& Value)
{
return Value.ToString();
}

template<typename T>
static typename TEnableIf<THasToStringFunc<T>::Value, FString>::Type ToString(const T* Value)
{
return Value ? Value->ToString() : TEXT("Invalid");
}

template<typename T>
static typename TEnableIf<TIsDerivedFrom<T, UObject>::Value && !THasToStringFunc<T>::Value, FString>::Type ToString(const T& Value)
{
if constexpr (THasGetUIDFunc<T>::Value)
{
return ToString(Value.GetUID());
}
else
{
return Value.GetName();
}
}

template<typename T>
static typename TEnableIf<TIsDerivedFrom<T, UObject>::Value && !THasToStringFunc<T>::Value, FString>::Type ToString(const T* Value)
{
if (!IsValid(Value)) return TEXT("Invalid");

if constexpr (THasGetUIDFunc<T>::Value)
{
return ToString(Value->GetUID());
}
else
{
return Value->GetName();
}
}

这里用了 THasToStringFuncTGetUIDFunc,需要新增 TypeTraits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct TrueType { static constexpr bool Value = true; };
struct FalseType { static constexpr bool Value = false; };

// THas[FunctionName]Func
// Declare type traits to detect if type T has a function named FunctionName with params.

#define DECLARE_HASFUNCTION_TYPETRAITS(FunctionName, ReturnType, ...) \
template<typename T> \
struct THas##FunctionName##Func\
{ \
private: \
template<typename, typename... TArgs> \
struct Internal { \
template<typename U, typename = void> \
struct THas##FunctionName##FuncInternal : FalseType {}; \
template<typename U> \
struct THas##FunctionName##FuncInternal<U, typename TEnableIf<TIsSame<ReturnType, decltype(DeclVal<U>().FunctionName(DeclVal<TArgs>()...))>::Value>::Type> : TrueType {}; \
}; \
public: \
static constexpr bool Value = Internal<void, ##__VA_ARGS__>::template THas##FunctionName##FuncInternal<T>::Value; \
};

// ----- Declare -----

// THasToStringFunc: FString T::ToString()
DECLARE_HASFUNCTION_TYPETRAITS(ToString, FString)
// THasGetUIDFunc: uint64 T::GetUID()
DECLARE_HASFUNCTION_TYPETRAITS(GetUID, uint64)

Pointer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public:
template<typename T>
static typename TEnableIf<TIsPointer<T>::Value, FString>::Type ToString(const T& Value)
{
return Value ? UStringUtils::ToString(*Value) : TEXT("Invalid");
}

template<typename T>
static FString ToString(TStrongObjectPtr<T> Value)
{
return Value.IsValid() ? UStringUtils::ToString(Value.Get()) : TEXT("Invalid");
}

template<typename T>
static FString ToString(TWeakPtr<T> Value)
{
return Value.IsValid() ? UStringUtils::ToString(Value.Pin().Get()) : TEXT("Invalid");
}

template<typename T>
static FString ToString(TSharedPtr<T> Value)
{
return Value.IsValid() ? UStringUtils::ToString(Value.Get()) : TEXT("Invalid");
}

template<typename T>
static FString ToString(const TUniquePtr<T>& Value)
{
return Value.IsValid() ? UStringUtils::ToString(Value.Get()) : TEXT("Invalid");
}

Variant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<typename T>
static typename TEnableIf<TIsSame<T, FVariant>::Value, FString>::Type ToString(const T& Value)
{
auto VariantType = Value.GetType();
switch (VariantType)
{
case EVariantTypes::Bool: return UStringUtils::ToString(Value.template GetValue<bool>());
case EVariantTypes::Float: return UStringUtils::ToString(Value.template GetValue<float>());
case EVariantTypes::String: return UStringUtils::ToString(Value.template GetValue<FString>());
case EVariantTypes::Int8: return UStringUtils::ToString(Value.template GetValue<int8>());
case EVariantTypes::Int16: return UStringUtils::ToString(Value.template GetValue<int16>());
case EVariantTypes::Int32: return UStringUtils::ToString(Value.template GetValue<int32>());
case EVariantTypes::Int64: return UStringUtils::ToString(Value.template GetValue<int64>());
case EVariantTypes::UInt8: return UStringUtils::ToString(Value.template GetValue<uint8>());
case EVariantTypes::UInt16: return UStringUtils::ToString(Value.template GetValue<uint16>());
case EVariantTypes::UInt32: return UStringUtils::ToString(Value.template GetValue<uint32>());
case EVariantTypes::UInt64: return UStringUtils::ToString(Value.template GetValue<uint64>());
}

return TEXT("Unknown");
}

Container

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
template<typename TContainer, typename TFunctor, typename = typename TEnableIf<TIsContainer<TContainer>::Value>::Type>
static FString ContainerToStringHelper(const TContainer& Container, const FString& StartToken, const FString& EndToken, bool Multilines, TFunctor&& ElementToStringFunctor)
{
if (Container.IsEmpty()) return TEXT("Empty");

TArray<FString> ElementStrings{};

for (const auto& Element : Container)
{
auto String = ElementToStringFunctor(Element);

if (String.IsEmpty())
{
ElementStrings.Emplace(TEXT("None"));
}
else
{
ElementStrings.Emplace(MoveTemp(String));
}
}

auto Res = FString::Join(ElementStrings, Multilines ? TEXT(",\n") : TEXT(", "));

if (Multilines)
{
return FString::Printf(TEXT("%s\n%s\n%s"), *StartToken, *Res, *EndToken);
}
else
{
return FString::Printf(TEXT("%s%s%s"), *StartToken, *Res, *EndToken);
}
}

template<typename T>
static FString ToString(const TArray<T>& Array, bool Multilines = false)
{
return UStringUtils::ContainerToStringHelper(Array, TEXT("["), TEXT("]"), Multilines, [](const auto& Element) -> FString {
return UStringUtils::ToString(Element);
});
}

template<typename T>
static FString ToString(const TSet<T>& Set, bool Multilines = false)
{
return UStringUtils::ContainerToStringHelper(Set, TEXT("("), TEXT(")"), Multilines, [](const auto& Element) -> FString {
return UStringUtils::ToString(Element);
});
}

template<typename TKey, typename TValue>
static FString ToString(const TMap<TKey, TValue>& Map, bool Multilines = false)
{
return UStringUtils::ContainerToStringHelper(Map, TEXT("{"), TEXT("}"), Multilines, [](const auto& Element) -> FString {
return FString::Printf(TEXT("%s: %s"), *UStringUtils::ToString(Element.Key), *UStringUtils::ToString(Element.Value));
});
}

这里用到了 TIsContainer ,可以通过是否有 .begin().end().IsEmpty() 方法简单判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// TMakeVoid
template<typename... T>
struct TMakeVoid { using Type = void; };

// TIsContainer
template<typename T, typename = void>
struct TIsContainer : FalseType {};

template<typename T>
struct TIsContainer<T, typename TMakeVoid<
decltype(DeclVal<T>().begin()),
decltype(DeclVal<T>().end()),
typename TEnableIf<TIsSame<decltype(DeclVal<T>().IsEmpty()), bool>::Value>::Type
>::Type> : TrueType {};

完整实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
// StringUtils.h

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "UObject/StrongObjectPtr.h"
#include "TypeTraits.h"
#include "StringUtils.generated.h"

UCLASS()
class UStringUtils : public UBlueprintFunctionLibrary
{
GENERATED_BODY()

#pragma region Base
public:
template<typename T>
static typename TEnableIf<TIsIntegral<T>::Value, FString>::Type ToString(const T& Value)
{
return FString::FromInt(static_cast<int32>(Value));
}

static FString ToString(const uint32& Value)
{
return FString::Printf(TEXT("%u"), Value);
}

static FString ToString(const int64& Value)
{
return FString::Printf(TEXT("%lld"), Value);
}

static FString ToString(const uint64& Value)
{
return FString::Printf(TEXT("%llu"), Value);
}

template<typename T>
static typename TEnableIf<TIsFloatingPoint<T>::Value, FString>::Type ToString(const T& Value)
{
return FString::Printf(TEXT("%.3f"), static_cast<float>(Value));
}

static FString ToString(const bool& Value)
{
return FString::Printf(TEXT("%s"), Value ? TEXT("true") : TEXT("false"));
}

static FString ToString(const FString& Value)
{
return Value;
}

#pragma endregion Base



#pragma region Enum
template<typename T>
static typename TEnableIf<TIsEnum<T>::Value, FString>::Type ToString(const T& Value)
{
if constexpr (google::protobuf::is_proto_enum<T>::value)
{
auto String = ::google::protobuf::internal::NameOfEnum(::google::protobuf::template GetEnumDescriptor<T>(), static_cast<int>(Value));
return UTF8_TO_TCHAR(String.c_str());
}
else if constexpr (TIsUEnumClass<T>::Value)
{
auto EnumClass = StaticEnum<T>();
return EnumClass->GetNameStringByValue(static_cast<int64>(Value));
}
else
{
return FString::Printf(TEXT("RawEnum_%s"), *ToString(static_cast<__underlying_type(T)>(Value)));
}
}
template<typename T>
static typename TEnableIf<TIsEnum<T>::Value, FString>::Type ToString(const TEnumAsByte<T>& Value)
{
return ToString(Value.GetValue());
}
#pragma endregion Enum


#pragma region ToString
public:
template<typename T>
static typename TEnableIf<THasToStringFunc<T>::Value, FString>::Type ToString(const T& Value)
{
return Value.ToString();
}

template<typename T>
static typename TEnableIf<THasToStringFunc<T>::Value, FString>::Type ToString(const T* Value)
{
return Value ? Value->ToString() : TEXT("Invalid");
}

template<typename T>
static typename TEnableIf<TIsDerivedFrom<T, UObject>::Value && !THasToStringFunc<T>::Value, FString>::Type ToString(const T& Value)
{
if constexpr (THasGetUIDFunc<T>::Value)
{
return ToString(Value.GetUID());
}
else
{
return Value.GetName();
}
}

template<typename T>
static typename TEnableIf<TIsDerivedFrom<T, UObject>::Value && !THasToStringFunc<T>::Value, FString>::Type ToString(const T* Value)
{
if (!IsValid(Value)) return TEXT("Invalid");

if constexpr (THasGetUIDFunc<T>::Value)
{
return ToString(Value->GetUID());
}
else
{
return Value->GetName();
}
}

#pragma endregion ToString


#pragma region Pointer
public:
template<typename T>
static typename TEnableIf<TIsPointer<T>::Value, FString>::Type ToString(const T& Value)
{
return Value ? UStringUtils::ToString(*Value) : TEXT("Invalid");
}

template<typename T>
static FString ToString(TStrongObjectPtr<T> Value)
{
return Value.IsValid() ? UStringUtils::ToString(Value.Get()) : TEXT("Invalid");
}

template<typename T>
static FString ToString(TWeakPtr<T> Value)
{
return Value.IsValid() ? UStringUtils::ToString(Value.Pin().Get()) : TEXT("Invalid");
}

template<typename T>
static FString ToString(TSharedPtr<T> Value)
{
return Value.IsValid() ? UStringUtils::ToString(Value.Get()) : TEXT("Invalid");
}

template<typename T>
static FString ToString(const TUniquePtr<T>& Value)
{
return Value.IsValid() ? UStringUtils::ToString(Value.Get()) : TEXT("Invalid");
}
#pragma endregion Pointer


#pragma region FVariant
public:
template<typename T>
static typename TEnableIf<TIsSame<T, FVariant>::Value, FString>::Type ToString(const T& Value)
{
auto VariantType = Value.GetType();
switch (VariantType)
{
case EVariantTypes::Bool: return UStringUtils::ToString(Value.template GetValue<bool>());
case EVariantTypes::Float: return UStringUtils::ToString(Value.template GetValue<float>());
case EVariantTypes::String: return UStringUtils::ToString(Value.template GetValue<FString>());
case EVariantTypes::Int8: return UStringUtils::ToString(Value.template GetValue<int8>());
case EVariantTypes::Int16: return UStringUtils::ToString(Value.template GetValue<int16>());
case EVariantTypes::Int32: return UStringUtils::ToString(Value.template GetValue<int32>());
case EVariantTypes::Int64: return UStringUtils::ToString(Value.template GetValue<int64>());
case EVariantTypes::UInt8: return UStringUtils::ToString(Value.template GetValue<uint8>());
case EVariantTypes::UInt16: return UStringUtils::ToString(Value.template GetValue<uint16>());
case EVariantTypes::UInt32: return UStringUtils::ToString(Value.template GetValue<uint32>());
case EVariantTypes::UInt64: return UStringUtils::ToString(Value.template GetValue<uint64>());
}

return TEXT("Unknown");
}
#pragma endregion FVariant


#pragma region Container
public:
template<typename TContainer, typename TFunctor, typename = typename TEnableIf<TIsContainer<TContainer>::Value>::Type>
static FString ContainerToStringHelper(const TContainer& Container, const FString& StartToken, const FString& EndToken, bool Multilines, TFunctor&& ElementToStringFunctor)
{
if (Container.IsEmpty()) return TEXT("Empty");

TArray<FString> ElementStrings{};

for (const auto& Element : Container)
{
auto String = ElementToStringFunctor(Element);

if (String.IsEmpty())
{
ElementStrings.Emplace(TEXT("None"));
}
else
{
ElementStrings.Emplace(MoveTemp(String));
}
}

auto Res = FString::Join(ElementStrings, Multilines ? TEXT(",\n") : TEXT(", "));

if (Multilines)
{
return FString::Printf(TEXT("%s\n%s\n%s"), *StartToken, *Res, *EndToken);
}
else
{
return FString::Printf(TEXT("%s%s%s"), *StartToken, *Res, *EndToken);
}
}

template<typename T>
static FString ToString(const TArray<T>& Array, bool Multilines = false)
{
return UStringUtils::ContainerToStringHelper(Array, TEXT("["), TEXT("]"), Multilines, [](const auto& Element) -> FString {
return UStringUtils::ToString(Element);
});
}

template<typename T>
static FString ToString(const TSet<T>& Set, bool Multilines = false)
{
return UStringUtils::ContainerToStringHelper(Set, TEXT("("), TEXT(")"), Multilines, [](const auto& Element) -> FString {
return UStringUtils::ToString(Element);
});
}

template<typename TKey, typename TValue>
static FString ToString(const TMap<TKey, TValue>& Map, bool Multilines = false)
{
return UStringUtils::ContainerToStringHelper(Map, TEXT("{"), TEXT("}"), Multilines, [](const auto& Element) -> FString {
return FString::Printf(TEXT("%s: %s"), *UStringUtils::ToString(Element.Key), *UStringUtils::ToString(Element.Value));
});
}
#pragma endregion Container

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// TypeTraits.h
#pragma once

#include "CoreMinimal.h"

struct TrueType { static constexpr bool Value = true; };
struct FalseType { static constexpr bool Value = false; };

// TMakeVoid
template<typename... T>
struct TMakeVoid { using Type = void; };

#pragma region Container

// TIsContainer
template<typename T, typename = void>
struct TIsContainer : FalseType {};

template<typename T>
struct TIsContainer<T, typename TMakeVoid<
decltype(DeclVal<T>().begin()),
decltype(DeclVal<T>().end()),
typename TEnableIf<TIsSame<decltype(DeclVal<T>().IsEmpty()), bool>::Value>::Type
>::Type> : TrueType {};

#pragma endregion Container


#pragma region HasMemberFunction

// THas[FunctionName]Func
// Declare type traits to detect if type T has a function named FunctionName with params.

#define DECLARE_HASFUNCTION_TYPETRAITS(FunctionName, ReturnType, ...) \
template<typename T> \
struct THas##FunctionName##Func\
{ \
private: \
template<typename, typename... TArgs> \
struct Internal { \
template<typename U, typename = void> \
struct THas##FunctionName##FuncInternal : FalseType {}; \
template<typename U> \
struct THas##FunctionName##FuncInternal<U, typename TEnableIf<TIsSame<ReturnType, decltype(DeclVal<U>().FunctionName(DeclVal<TArgs>()...))>::Value>::Type> : TrueType {}; \
}; \
public: \
static constexpr bool Value = Internal<void, ##__VA_ARGS__>::template THas##FunctionName##FuncInternal<T>::Value; \
};

// ----- Declare -----

// THasToStringFunc: FString T::ToString()
DECLARE_HASFUNCTION_TYPETRAITS(ToString, FString)
// THasGetUIDFunc: uint64 T::GetUID()
DECLARE_HASFUNCTION_TYPETRAITS(GetUID, uint64)

#pragma endregion HasMemberFunction